Цель исследования - проанализировать список гипотез, подготовленных вместе с отделом маркетинга, для увеличения выручки.
Задачи иследования:
Описание данных:
Данные для первой части
Файл /datasets/hypothesis.csv.
Hypothesis — краткое описание гипотезы;
Reach — охват пользователей по 10-балльной шкале;
Impact — влияние на пользователей по 10-балльной шкале;
Confidence — уверенность в гипотезе по 10-балльной шкале;
Efforts — затраты ресурсов на проверку гипотезы по 10-балльной шкале. Чем больше значение Efforts, тем дороже проверка гипотезы.
Данные для второй части
Файл /datasets/orders.csv.
transactionId — идентификатор заказа;
visitorId — идентификатор пользователя, совершившего заказ;
date — дата, когда был совершён заказ;
revenue — выручка заказа;
group — группа A/B-теста, в которую попал заказ.
Файл /datasets/visitors.csv.
date — дата;
group — группа A/B-теста;
visitors — количество пользователей в указанную дату в указанной группе A/B-теста.
Ход исследования:
1.1 Импорт необходимых библиотек
import pandas as pd
import scipy.stats as stats
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats as stp
import plotly.express as px
1.2 Загрузка и чтение данных из csv-файла в датафрейм c помощью библиотеки pandas
hypothesis, orders, visitors = (
pd.read_csv('//datasets/hypothesis.csv'),
pd.read_csv('/datasets/orders.csv'),
pd.read_csv('/datasets/visitors.csv'),
)
#создание функции для чтение первичной информации для датасетов
def data_full_info(dataset):
print (dataset.info())
print('\033[1m' + 'Количество дубликатов в таблице:' + '\033[0m', dataset.duplicated().sum())
return (dataset.head(10))
data_full_info(hypothesis)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9 entries, 0 to 8
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Hypothesis 9 non-null object
1 Reach 9 non-null int64
2 Impact 9 non-null int64
3 Confidence 9 non-null int64
4 Efforts 9 non-null int64
dtypes: int64(4), object(1)
memory usage: 488.0+ bytes
None
Количество дубликатов в таблице: 0
| Hypothesis | Reach | Impact | Confidence | Efforts | |
|---|---|---|---|---|---|
| 0 | Добавить два новых канала привлечения трафика,... | 3 | 10 | 8 | 6 |
| 1 | Запустить собственную службу доставки, что сок... | 2 | 5 | 4 | 10 |
| 2 | Добавить блоки рекомендаций товаров на сайт ин... | 8 | 3 | 7 | 3 |
| 3 | Изменить структура категорий, что увеличит кон... | 8 | 3 | 3 | 8 |
| 4 | Изменить цвет фона главной страницы, чтобы уве... | 3 | 1 | 1 | 1 |
| 5 | Добавить страницу отзывов клиентов о магазине,... | 3 | 2 | 2 | 3 |
| 6 | Показать на главной странице баннеры с актуаль... | 5 | 3 | 8 | 3 |
| 7 | Добавить форму подписки на все основные страни... | 10 | 7 | 8 | 5 |
| 8 | Запустить акцию, дающую скидку на товар в день... | 1 | 9 | 9 | 5 |
data_full_info(orders)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1197 entries, 0 to 1196
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 transactionId 1197 non-null int64
1 visitorId 1197 non-null int64
2 date 1197 non-null object
3 revenue 1197 non-null int64
4 group 1197 non-null object
dtypes: int64(3), object(2)
memory usage: 46.9+ KB
None
Количество дубликатов в таблице: 0
| transactionId | visitorId | date | revenue | group | |
|---|---|---|---|---|---|
| 0 | 3667963787 | 3312258926 | 2019-08-15 | 1650 | B |
| 1 | 2804400009 | 3642806036 | 2019-08-15 | 730 | B |
| 2 | 2961555356 | 4069496402 | 2019-08-15 | 400 | A |
| 3 | 3797467345 | 1196621759 | 2019-08-15 | 9759 | B |
| 4 | 2282983706 | 2322279887 | 2019-08-15 | 2308 | B |
| 5 | 182168103 | 935554773 | 2019-08-15 | 2210 | B |
| 6 | 398296753 | 2900797465 | 2019-08-15 | 1860 | B |
| 7 | 2626614568 | 78758296 | 2019-08-15 | 1044 | A |
| 8 | 1576988021 | 295230930 | 2019-08-15 | 13710 | A |
| 9 | 1506739906 | 1882260405 | 2019-08-15 | 1855 | B |
data_full_info(visitors)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 62 entries, 0 to 61
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 62 non-null object
1 group 62 non-null object
2 visitors 62 non-null int64
dtypes: int64(1), object(2)
memory usage: 1.6+ KB
None
Количество дубликатов в таблице: 0
| date | group | visitors | |
|---|---|---|---|
| 0 | 2019-08-01 | A | 719 |
| 1 | 2019-08-02 | A | 619 |
| 2 | 2019-08-03 | A | 507 |
| 3 | 2019-08-04 | A | 717 |
| 4 | 2019-08-05 | A | 756 |
| 5 | 2019-08-06 | A | 667 |
| 6 | 2019-08-07 | A | 644 |
| 7 | 2019-08-08 | A | 610 |
| 8 | 2019-08-09 | A | 617 |
| 9 | 2019-08-10 | A | 406 |
3 дата-фрейма хранят в себе данные о гипотезах, совершенных заказов и пользователях, разделенных на группы для тестирования. Во всех датасетах не наблюдается пропусков и явных дубликатов. Необходимо внести изменения в названия столбцов таблиц для комфортной работы и изменить типы данных, что важно для последующего анализа.
1.3 Предобработка данных
Переименование столбцов в соотвествии со стилем snake_case.
hypothesis = hypothesis.rename(columns=str.lower)
orders = orders.rename(columns={'transactionId': 'transaction_id', 'visitorId': 'visitor_id'})
Изменение типа данных необходимых столбцов.
orders['date'] = orders['date'].map(
lambda x: dt.datetime.strptime(x, '%Y-%m-%d')
)
visitors['date'] = visitors['date'].map(
lambda x: dt.datetime.strptime(x, '%Y-%m-%d')
)
Изучение временного интервала(даты начала и окончания теста)
print('Дата начала теста:', orders['date'].min())
print('Дата окончания теста:', orders['date'].max())
Дата начала теста: 2019-08-01 00:00:00 Дата окончания теста: 2019-08-31 00:00:00
Тестирование проходило с 1 августа 2019 г. по 31 августа 2019г.
fig = px.line(visitors.pivot_table(index='date', values='visitors', aggfunc='sum', columns='group'),
template='plotly_white', title='Динамика посетителей по дням и по группам',
labels=dict(date="дата", value="Количество пользователей в день", group="группа"))
fig.show()
По динамике посетителей по дням и по группам видно, что количество пользователей в обеих группах было достаточно равноценным, периоды спада и подъема в обоих группах приходятся в большинстве случаев на одну и ту же датую
Разделение пользователей по группам и проверка пользователей на попадание в обе группы.
group_a = orders[orders['group'] == 'A']['visitor_id']
group_b = orders[orders['group'] == 'B']['visitor_id']
orders_ab = orders.query('visitor_id in @group_a and visitor_id in @group_b')
display(orders_ab['visitor_id'].unique())
print('Количество повторяющихся пользователей в двух группах:', orders_ab['visitor_id'].nunique())
print('Количество человек в группе A:', group_a.count())
print('Количество человек в группе B:', group_b.count())
print('Всего пользователей в тесте:', orders['visitor_id'].count())
array([4069496402, 963407295, 351125977, 3234906277, 199603092,
237748145, 3803269165, 2038680547, 2378935119, 4256040402,
2712142231, 8300375, 276558944, 457167155, 3062433592,
1738359350, 2458001652, 2716752286, 3891541246, 1648269707,
3656415546, 2686716486, 2954449915, 2927087541, 2579882178,
3957174400, 2780786433, 3984495233, 818047933, 1668030113,
3717692402, 2044997962, 1959144690, 1294878855, 1404934699,
2587333274, 3202540741, 1333886533, 2600415354, 3951559397,
393266494, 3972127743, 4120364173, 4266935830, 1230306981,
1614305549, 477780734, 1602967004, 1801183820, 4186807279,
3766097110, 3941795274, 471551937, 1316129916, 232979603,
2654030115, 3963646447, 2949041841])
Количество повторяющихся пользователей в двух группах: 58 Количество человек в группе A: 557 Количество человек в группе B: 640 Всего пользователей в тесте: 1197
Так как пересекающиеся пользователи в группах A и B могут оказать влияние на проведение дальнейшего тестирования, следует отчистить датасет от таких пользователей. Однако, после удаление пересекающихся данных стоит относится осторожно к результатм теста. Такой случай мог являться результатом некорректного проведение A/A-теста или его отсутствия. В будущем перед проведение A/B-теста стоит более внимательно отнестись к A/A-тесту, т.к. качество данных для дальшейшего тестирования будет лучше.
orders = orders.query('visitor_id not in @orders_ab["visitor_id"]')
print('Количество пользователей после обработки:', orders['visitor_id'].count())
Количество пользователей после обработки: 1016
Данные предобработаны и готовы к проведению A/B-тестированию и проверке гипотез.
В компании принята шкала линейной оценки гипотезы от 0 до 10, где 0 — «охват гипотезы ни на кого не повлияет», а 10 — «гипотеза охватит хотя бы половину пользователей».
2.1 Применение фреймворка ICE для приоритизации гипотез
hypothesis['ICE'] = round(hypothesis['impact']*hypothesis['confidence']/hypothesis['efforts'], 2)
# Сброс ограничений на количество символов в записи
pd.set_option('display.max_colwidth', None)
hypothesis[['hypothesis', 'ICE']].sort_values(by='ICE', ascending= False)
| hypothesis | ICE | |
|---|---|---|
| 8 | Запустить акцию, дающую скидку на товар в день рождения | 16.20 |
| 0 | Добавить два новых канала привлечения трафика, что позволит привлекать на 30% больше пользователей | 13.33 |
| 7 | Добавить форму подписки на все основные страницы, чтобы собрать базу клиентов для email-рассылок | 11.20 |
| 6 | Показать на главной странице баннеры с актуальными акциями и распродажами, чтобы увеличить конверсию | 8.00 |
| 2 | Добавить блоки рекомендаций товаров на сайт интернет магазина, чтобы повысить конверсию и средний чек заказа | 7.00 |
| 1 | Запустить собственную службу доставки, что сократит срок доставки заказов | 2.00 |
| 5 | Добавить страницу отзывов клиентов о магазине, что позволит увеличить количество заказов | 1.33 |
| 3 | Изменить структура категорий, что увеличит конверсию, т.к. пользователи быстрее найдут нужный товар | 1.12 |
| 4 | Изменить цвет фона главной страницы, чтобы увеличить вовлеченность пользователей | 1.00 |
По фреймворку ICE приоритетнее оказались гипотезы:
2.2 Применение фреймворка RICE для приоритизации гипотез
hypothesis['RICE'] = (hypothesis['reach'] * hypothesis['impact'] * hypothesis['confidence']) / hypothesis['efforts']
# Сброс ограничений на количество символов в записи
pd.set_option('display.max_colwidth', None)
hypothesis[['hypothesis', 'RICE']].sort_values(by='RICE', ascending=False)
| hypothesis | RICE | |
|---|---|---|
| 7 | Добавить форму подписки на все основные страницы, чтобы собрать базу клиентов для email-рассылок | 112.0 |
| 2 | Добавить блоки рекомендаций товаров на сайт интернет магазина, чтобы повысить конверсию и средний чек заказа | 56.0 |
| 0 | Добавить два новых канала привлечения трафика, что позволит привлекать на 30% больше пользователей | 40.0 |
| 6 | Показать на главной странице баннеры с актуальными акциями и распродажами, чтобы увеличить конверсию | 40.0 |
| 8 | Запустить акцию, дающую скидку на товар в день рождения | 16.2 |
| 3 | Изменить структура категорий, что увеличит конверсию, т.к. пользователи быстрее найдут нужный товар | 9.0 |
| 1 | Запустить собственную службу доставки, что сократит срок доставки заказов | 4.0 |
| 5 | Добавить страницу отзывов клиентов о магазине, что позволит увеличить количество заказов | 4.0 |
| 4 | Изменить цвет фона главной страницы, чтобы увеличить вовлеченность пользователей | 3.0 |
По фреймворку RICE приоритетнее оказались гипотезы:
Наиболее перспективные гипотезы совпадают после применение фреймворков ICE и RICE. Однако на первом месте уже гипотеза под номером 7. Так получилось потому, что ее параметр Reach (охват пользователей) равен максимальному количеству баллов - 10, в то время как у других гипотез он меньше, ведь кроме силы гитопез стоит учитывать то, как много пользователей она затронет.
Следовательно, акцентируя внимание на результатах применение фреймворка RICE и качеству охвата пользователей, стоит в перспективе обратить внимание на гипотезы 7, 2 и 0.
3.1 График кумулятивной выручки по группам
# создаем массив уникальных пар значений дат и групп теста
dates_groups = orders[['date','group']].drop_duplicates()
# получаем агрегированные кумулятивные по дням данные о заказах
orders_aggregated = dates_groups.apply(lambda x: orders[np.logical_and(orders['date'] <= x['date'], \
orders['group'] == x['group'])]
.agg({'date' : 'max', 'group' : 'max', 'transaction_id' : 'nunique', \
'visitor_id' : 'nunique', 'revenue' : 'sum'}), axis=1).sort_values(by=['date','group'])
# получаем агрегированные кумулятивные по дням данные о посетителях интернет-магазина
visitors_aggregated = dates_groups.apply(lambda x: visitors[np.logical_and(visitors['date'] <= x['date'],\
visitors['group'] == x['group'])]
.agg({'date' : 'max', 'group' : 'max', 'visitors' : 'sum'}), axis=1).sort_values(by=['date','group'])
# объединяем кумулятивные данные в одной таблице и присваиваем ее столбцам понятные названия
cumulative_data = orders_aggregated.merge(visitors_aggregated, left_on=['date', 'group'], right_on=['date', 'group'])
cumulative_data.columns = ['date', 'group', 'orders', 'buyers', 'revenue', 'visitors']
print(cumulative_data.head(5))
date group orders buyers revenue visitors 0 2019-08-01 A 23 19 142779 719 1 2019-08-01 B 17 17 59758 713 2 2019-08-02 A 42 36 234381 1338 3 2019-08-02 B 40 39 221801 1294 4 2019-08-03 A 66 60 346854 1845
# датафрейм с кумулятивным количеством заказов и кумулятивной выручкой по дням в группе А
cumulative_revenue_a = cumulative_data[cumulative_data['group']=='A'][['date','revenue', 'orders']]
# датафрейм с кумулятивным количеством заказов и кумулятивной выручкой по дням в группе B
cumulative_revenue_b = cumulative_data[cumulative_data['group']=='B'][['date','revenue', 'orders']]
#зададим размеры графика
plt.figure(figsize=(12,8))
# Строим график выручки группы А
plt.plot(cumulative_revenue_a['date'], cumulative_revenue_a['revenue'], label='A')
# Строим график выручки группы B
plt.plot(cumulative_revenue_b['date'], cumulative_revenue_b['revenue'], label='B')
plt.title('График кумулятивной выручки по группам')
plt.xlabel('Дата')
plt.ylabel('Выручка')
plt.legend()
plt.show()
Прибыль растет в течение всего рассматриваемого периода. В группе В прибыль растет быстрее группы А на протяжении всего периода.
В некоторых точках видны всплески, особенно ярко выделяется яркий скачок показателей группы В около 19 августа 2019г. Видимо выросло количество заказов или произошел аномальный всплеск, благодаря рекламной кампании или акции.
Скорее всего, принимать какие-либо решения по данной метрике дано, требуюется дополнительный анализ выбросов, которые так сильно исказили результаты.
3.2 График кумулятивного среднего чека по группам
#зададим размеры графика
plt.figure(figsize=(12,8))
#построим графики, что бы получить средний чек, разделим выручку на число заказов
plt.plot(cumulative_revenue_a['date'], cumulative_revenue_a['revenue']/cumulative_revenue_a['orders'], label='A')
plt.plot(cumulative_revenue_b['date'], cumulative_revenue_b['revenue']/cumulative_revenue_b['orders'], label='B')
plt.title('График кумулятивного среднего чека по группам')
plt.xlabel('Дата')
plt.ylabel('Средний чек')
plt.legend()
plt.show()
Показатели кумулятивного среднего чека по группам росли неравномерно.
В группе А заметен резкий спад в начале периода исследования, следом подъем к середине, потом незначительное падение и дальнейшее стабильное движение почти без изменений.
В группе В наблюдаются более высокие показатели по отношению к группе А, а также резкие взлеты. Один из которых приходится на описываемый выше период (около 19 августа 2019г.). Быросы, повлиявшие данные в целом, при заключительном анализе необходимо будет удалить
К концу периода в группе В идет тенденция к спаду, в то время как у группы А наоборот - к небольшому росту.
3.3 График относительного изменения кумулятивного среднего чека группы B к группе A
# собираем данные в одном датафрейме
merged_cumulative_revenue = cumulative_revenue_a.merge(cumulative_revenue_b, left_on='date', \
right_on='date', how='left', suffixes=['A', 'B'])
#зададим размеры графика
plt.figure(figsize=(12,8))
# cтроим отношение средних чеков
plt.plot(merged_cumulative_revenue['date'], (merged_cumulative_revenue['revenueB']/merged_cumulative_revenue['ordersB']) \
/(merged_cumulative_revenue['revenueA']/merged_cumulative_revenue['ordersA'])-1)
# добавляем ось X
plt.axhline(y=0, color='black', linestyle='--')
plt.title('График относительного изменения кумулятивного среднего чека группы B к группе A')
plt.xlabel('Дата')
plt.ylabel('Доля')
plt.show()
На графике видны резкие изменения среднего чека и явное отличие среднего чека группы В по отношению к группы А. Результаты теста значительно и резко менялись в несколько дат. Скорее всего, причиной резких скачков стали произведенные пользователями аномально затратные заказы.
3.4 График кумулятивного среднего количества заказов на посетителя по группам
# считаем среднее количество заказов на посетителя
cumulative_data['conversion'] = cumulative_data['orders']/cumulative_data['visitors']
# отделяем данные по группе A
cumulative_data_a = cumulative_data[cumulative_data['group']=='A']
# отделяем данные по группе B
cumulative_data_b = cumulative_data[cumulative_data['group']=='B']
#зададим размеры графика
plt.figure(figsize=(12,8))
# строим графики
plt.plot(cumulative_data_a['date'], cumulative_data_a['conversion'], label='A')
plt.plot(cumulative_data_b['date'], cumulative_data_b['conversion'], label='B')
plt.title('График кумулятивного среднего количества заказов на посетителя по группам')
plt.xlabel('Дата')
plt.ylabel('среднее количество заказов на посетителя')
plt.legend()
plt.show()
На графике видно, что в первые дни показатели у группы А были выше, чем у группы В, но спустя 6 дней со времени запуска теста, это поменялось. У группы А наблюдается постепенный спад, с равномерными колебаниями. А у группы В начался период подъема с достаточно сильными колебаниями, однако ближе к концу исследуемого периода показатели выравниваются.
3.5 График относительного изменения кумулятивного среднего количества заказов на посетителя группы B к группе A
merged_cumulative_conversions = cumulative_data_a[['date','conversion']].merge(cumulative_data_b[['date','conversion']], \
left_on='date', right_on='date', how='left', \
suffixes=['A', 'B'])
#строим график
plt.figure(figsize=(15, 10))
plt.plot(merged_cumulative_conversions['date'], \
merged_cumulative_conversions['conversionB']/merged_cumulative_conversions['conversionA']-1, \
label="Относительный прирост конверсии группы B относительно группы A")
# добавляем оси
plt.axhline(y=0, color='black', linestyle='--')
plt.axhline(y=0.15, color='grey', linestyle='--')
plt.title('График относительного изменения кумулятивного среднего количества заказов на посетителя группы B к группе A')
plt.xlabel('Дата')
plt.ylabel('Среднее количество заказов на посетителя')
plt.legend()
plt.show()
По прошествию 5 дней теста группа В лидирует в метрике и становится все лучше. К концу периода показатели снижаются , но устаканиваются в показателе около 15% относительно группы А.
3.6 Точечный график количества заказов по пользователям
#Подсчитаем количество заказов по пользователям
orders_by_users = (
orders.groupby('visitor_id', as_index=False)
.agg({'transaction_id': 'nunique'})
)
orders_by_users.columns = ['visitor_id', 'orders']
print(orders_by_users.sort_values(by='orders', ascending=False).head(10))
plt.figure(figsize=(10, 7))
plt.hist(orders_by_users['orders'])
plt.xlabel('Количество заказов')
plt.ylabel('Число пользователей')
plt.title("Гистограмма распределения количества заказов на одного пользователя")
plt.show()
visitor_id orders 908 3967698036 3 55 249864742 3 478 2108163459 3 687 2988190573 3 890 3908431265 3 138 611059232 3 632 2742574263 3 157 678354126 2 323 1404560065 2 452 1985475298 2
Большинство покупателей заказывали только один раз.
#Строим график
x_values = pd.Series(range(0, len(orders_by_users)))
plt.figure(figsize=(10, 7))
plt.scatter(x_values, orders_by_users['orders'])
plt.xlabel('Пользователи')
plt.ylabel('Количество заказов')
plt.title("Количество заказов по пользователям")
plt.show()
По точечному графику видно, что в большинстве случаев пользователи совершали не больше 1-2 заказов.
3.7 95-й и 99-й перцентили количества заказов на пользователя
orders_perc = np.percentile(orders_by_users['orders'], [90, 95, 99])
print('Не более 5% пользователей совершает {} и более заказов.'.format(int(orders_perc[1])))
print('Не более 1% пользователей совершает {} и более заказов.'.format(int(orders_perc[2])))
Не более 5% пользователей совершает 1 и более заказов. Не более 1% пользователей совершает 2 и более заказов.
Границей при дальнейшем тестировании стоит брать - 99-й перцентиль.
3.8 Точечный график стоимостей заказов
x_values = pd.Series(range(0, len(orders['revenue'])))
plt.figure(figsize=(10, 7))
plt.scatter(x_values, orders['revenue'])
plt.xlabel('Пользователи')
plt.ylabel('Стоимость заказов')
plt.title("Стоимость заказов по пользователям")
plt.show()
По точечному графику видно, что в большинстве случаев пользователи совершали стоимостью около 20000.
3.9 95-й и 99-й перцентили стоимости заказов
revenue_perc = np.percentile(orders['revenue'], [90, 95, 99])
print('Не более 5% пользователей совершило заказ на сумму {} и более.'.format(int(revenue_perc[1])))
print('Не более 1% пользователей совершило заказ на сумму {} и более.'.format(int(revenue_perc[2])))
Не более 5% пользователей совершило заказ на сумму 26785 и более. Не более 1% пользователей совершило заказ на сумму 53904 и более.
Границей при дальнейшем тестировании стоит брать - показатель стоимости в 30000, что немногим больше 95-го перцентиля.
3.10 Статистическая значимость различий в среднем количестве заказов на посетителя между группами по «сырым» данным
Создаем переменные orders_by_users_a и orders_by_users_b со столбцами ['visitor_id', 'orders'], где для пользователей, совершивших хотя бы 1 заказ, будет указано число заказов. Объявляем переменные sample_a и sample_b, в которых пользователям с заказами будет соответствовать число заказов пользователя. А пользователям без заказов — нули. Считаем статистическую значимость различия среднего количества заказов по результатам теста. Применяем тест Манна-Уитни, с заданным уровнем значимости 0.05.
Гипотезы:
Н0: различий в среднем количестве заказов между группами нет.
Н1: различия в среднем между группами есть.
visitors_a_daily = visitors[visitors['group'] == 'A'][['date', 'visitors']]
visitors_a_daily.columns = ['date', 'visitors_per_date_a']
visitors_a_cummulative = visitors_a_daily.apply(
lambda x: visitors_a_daily[visitors_a_daily['date'] <= x['date']].agg(
{'date': 'max', 'visitors_per_date_a': 'sum'}
),
axis=1,
)
visitors_a_cummulative.columns = ['date', 'visitors_cummulative_a']
visitors_b_daily = visitors[visitors['group'] == 'B'][['date', 'visitors']]
visitors_b_daily.columns = ['date', 'visitors_per_date_b']
visitors_b_cummulative = visitors_b_daily.apply(
lambda x: visitors_b_daily[visitors_b_daily['date'] <= x['date']].agg(
{'date': 'max', 'visitors_per_date_b': 'sum'}
),
axis=1,
)
visitors_b_cummulative.columns = ['date', 'visitors_cummulative_b']
orders_a_daily = (
orders[orders['group'] == 'A'][['date', 'transaction_id', 'visitor_id', 'revenue']]
.groupby('date', as_index=False)
.agg({'transaction_id': pd.Series.nunique, 'revenue': 'sum'})
)
orders_a_daily.columns = ['date', 'orders_per_date_a', 'revenue_per_date_a']
orders_a_cummulative = orders_a_daily.apply(
lambda x: orders_a_daily[orders_a_daily['date'] <= x['date']].agg(
{'date': 'max', 'orders_per_date_a': 'sum', 'revenue_per_date_a': 'sum'}
),
axis=1,
).sort_values(by=['date'])
orders_a_cummulative.columns = [
'date',
'orders_cummulative_a',
'revenue_cummulative_a',
]
orders_b_daily = (
orders[orders['group'] == 'B'][['date', 'transaction_id', 'visitor_id', 'revenue']]
.groupby('date', as_index=False)
.agg({'transaction_id': pd.Series.nunique, 'revenue': 'sum'})
)
orders_b_daily.columns = ['date', 'orders_per_date_b', 'revenue_per_date_b']
orders_b_cummulative = orders_b_daily.apply(
lambda x: orders_b_daily[orders_b_daily['date'] <= x['date']].agg(
{'date': 'max', 'orders_per_date_b': 'sum', 'revenue_per_date_b': 'sum'}
),
axis=1,
).sort_values(by=['date'])
orders_b_cummulative.columns = [
'date',
'orders_cummulative_b',
'revenue_cummulative_b',
]
data = (
orders_a_daily.merge(
orders_b_daily, left_on='date', right_on='date', how='left'
)
.merge(orders_a_cummulative, left_on='date', right_on='date', how='left')
.merge(orders_b_cummulative, left_on='date', right_on='date', how='left')
.merge(visitors_a_daily, left_on='date', right_on='date', how='left')
.merge(visitors_b_daily, left_on='date', right_on='date', how='left')
.merge(visitors_a_cummulative, left_on='date', right_on='date', how='left')
.merge(visitors_b_cummulative, left_on='date', right_on='date', how='left')
)
orders_by_users_a = (
orders[orders['group'] == 'A']
.groupby('visitor_id', as_index=False)
.agg({'transaction_id': pd.Series.nunique})
)
orders_by_users_a.columns = ['visitor_id', 'orders']
orders_by_users_b = (
orders[orders['group'] == 'B']
.groupby('visitor_id', as_index=False)
.agg({'transaction_id': pd.Series.nunique})
)
orders_by_users_b.columns = ['visitor_id', 'orders']
pd.Series(0, index=np.arange(data['visitors_per_date_a'].sum() - len(orders_by_users_a['orders'])), name='orders')
[orders_by_users_a['orders'],pd.Series(0, index=np.arange(data['visitors_per_date_a'].sum() - len(orders_by_users_a['orders'])), name='orders')]
sample_a = pd.concat([orders_by_users_a['orders'],pd.Series(0, index=np.arange(data['visitors_per_date_a'].sum() - len(orders_by_users_a['orders'])), name='orders')],axis=0)
sample_b = pd.concat([orders_by_users_b['orders'],pd.Series(0, index=np.arange(data['visitors_per_date_b'].sum() - len(orders_by_users_b['orders'])), name='orders')],axis=0)
orders_by_users_a = (
orders[orders['group'] == 'A']
.groupby('visitor_id', as_index=False)
.agg({'transaction_id': pd.Series.nunique})
)
orders_by_users_a.columns = ['visitor_id', 'orders']
orders_by_users_b = (
orders[orders['group'] == 'B']
.groupby('visitor_id', as_index=False)
.agg({'transaction_id': pd.Series.nunique})
)
orders_by_users_b.columns = ['visitor_id', 'orders']
sample_a = pd.concat(
[
orders_by_users_a['orders'],
pd.Series(
0,
index=np.arange(
data['visitors_per_date_a'].sum() - len(orders_by_users_a['orders'])
),
name='orders',
),
],
axis=0,
)
sample_b = pd.concat(
[
orders_by_users_b['orders'],
pd.Series(
0,
index=np.arange(
data['visitors_per_date_b'].sum() - len(orders_by_users_b['orders'])
),
name='orders',
),
],
axis=0,
)
print('P-value:',"{0:.3f}".format(stats.mannwhitneyu(sample_a, sample_b)[1]))
print('Относительные различия:',"{0:.3f}".format(sample_b.mean() / sample_a.mean() - 1))
P-value: 0.011 Относительные различия: 0.160
P-value равно 1.1%, это меньше заданного уровня значимости. Поэтому мы можем отвергнуть нулевую гипотезу. Следственно, разница между группами по "сырым данным" присутствует.
Относительный прирост группы В по среднему количеству заказов на посетителя составил 16%.
3.11 Статистическая значимость различий в среднем чеке заказа между группами по «сырым» данным
Гипотезы:
H0:Средние чеки групп A и B равны
H1:Средние чеки групп A и B различны
print('P-value:','{0:.3f}'.format(stats.mannwhitneyu(orders[orders['group']=='A']['revenue'], orders[orders['group']=='B']['revenue'])[1]))
print('Относительные различия:','{0:.3f}'.format(orders[orders['group']=='B']['revenue'].mean()/orders[orders['group']=='A']['revenue'].mean()-1))
P-value: 0.829 Относительные различия: 0.287
P-value больше 0.05 - статистически значимых отличий в среднем чеке между группами нет.
Кроме того, относительное различие среднего чека между сегментами составило 29%.
3.12 Статистическая значимость различий в среднем количестве заказов на посетителя между группами по «очищенным» данным
При работе над "очищением" данных будем считать пользователей аномальными, если они совершали более 2 заказов на сумму более 30000.
users_with_many_orders = pd.concat(
[
orders_by_users_a[orders_by_users_a['orders'] > 2]['visitor_id'],
orders_by_users_b[orders_by_users_b['orders'] > 2]['visitor_id'],
],
axis=0,
)
users_with_expensive_orders = orders[orders['revenue'] > 30000]['visitor_id']
abnormal_users = (
pd.concat([users_with_many_orders, users_with_expensive_orders], axis=0)
.drop_duplicates()
.sort_values()
)
Создадим переменные sample_a_filtered и sample_b_filtered, в которых сохраним очищенные данные о заказах — не включая аномальных пользователей. Выведем p-value для сравнения среднего между очищенными группами. Округлим p-value до трех знаков после запятой. Выведем относительный прирост среднего очищенной группы B, округлив до трёх знаков после запятой.
sample_a_filtered = pd.concat(
[
orders_by_users_a[
np.logical_not(orders_by_users_a['visitor_id'].isin(abnormal_users))
]['orders'],
pd.Series(
0,
index=np.arange(
data['visitors_per_date_a'].sum() - len(orders_by_users_a['orders'])
),
name='orders',
),
],
axis=0,
)
sample_b_filtered = pd.concat(
[
orders_by_users_b[
np.logical_not(orders_by_users_b['visitor_id'].isin(abnormal_users))
]['orders'],
pd.Series(
0,
index=np.arange(
data['visitors_per_date_b'].sum() - len(orders_by_users_b['orders'])
),
name='orders',
),
],
axis=0,
)
print('P-value:','{0:.3f}'.format(stats.mannwhitneyu(sample_a_filtered, sample_b_filtered)[1]))
print('Относительные различия:','{0:.3f}'.format(sample_b_filtered.mean()/sample_a_filtered.mean()-1))
P-value: 0.010 Относительные различия: 0.187
Как и в случае с сырыми данными, статистическая значимость достигнута. Группа В лучше группы А.
3.13 Статистическая значимость различий в среднем чеке заказа между группами по «очищенным» данным
print('P-value:',
'{0:.3f}'.format(
stats.mannwhitneyu(
orders[
np.logical_and(
orders['group'] == 'A',
np.logical_not(orders['visitor_id'].isin(abnormal_users)),
)
]['revenue'],
orders[
np.logical_and(
orders['group'] == 'B',
np.logical_not(orders['visitor_id'].isin(abnormal_users)),
)
]['revenue'],
)[1]
)
)
print('Относительные различия:',
"{0:.3f}".format(
orders[
np.logical_and(
orders['group'] == 'B',
np.logical_not(orders['visitor_id'].isin(abnormal_users)),
)
]['revenue'].mean()
/ orders[
np.logical_and(
orders['group'] == 'A',
np.logical_not(orders['visitor_id'].isin(abnormal_users)),
)
]['revenue'].mean()
- 1
)
)
P-value: 0.741 Относительные различия: -0.027
Значение p-value уменьшилось с 0.829 до 0.741. Относительные различия уменьшились с 29% до 2,7%. Это может означать, что после удаления выбросов в данных стало меньше шума.
Как и в случае с "сырыми данными" статистически значимых отличий в среднем чеке между группами нет
Графики кумулятивных расчетов показали, что:
В данных присутствовали аномалии: пользователи, совершившие более 2 заказов на сумму более 30000.
Расчеты статистической значимости в среднем чеке заказа между группами по сырым и очищенным данным показали, что:
Расчеты статистической значимости в среднем количестве заказов между группами по сырым и очищенным данным показали, что:
Итог
Исхолдя из этого, тест следует остановить и зафиксировать победу группы В.
В ходе работы были выполнены следующие задачи: